//
// del.icio.us visualisation
//
// Joshua Schachter's del.icio.us enabled it all, but doesn't let us see the bigger picture.
// Matt Jones and Kevan Davis got there first by hand and html, but I think this has some promise.
//
// http://kevan.org/extispicious
// http://blackbeltjones.typepad.com/work/2004/07/my_delicious_ta.html
// http://del.icio.us/
//
// Currently using a hard-coded version of...
// posts.xml - http://del.icio.us/api/posts/recent?count=1000
// tags.xml - http://del.icio.us/api/tags/get?
//
// This won't be live until I redirect/cache my data with PHP to get around the Java sandbox.
// This won't show *your* data unless I use regular Java or if Processing can easily do http-auth.
//
// Please don't pick this code apart on your own.  If you email me, I might have commented it by now!
//
// tom (at) tom-carden (dot) co (dot) uk
//

// for the text...
BFont font;

// translations...
float sc = 1.0, tx = 300.0, ty = 300.0;

// 'physics', hmm...
float const_mass = 30.0,
      const_friction  = 0.9;

// picking...
boolean gotOne = false, // is something selected already?
        overOne = false; // is the mouse over something already?

class Node {
  float x, y, fx, fy, vx, vy;
  float mass;
  float radius;
  boolean mouseOver = false, selected = false;
  Node() {
    this(10.0, 5.0);
  }
  Node(float mass, float radius) {
    x = y = fx = fy = vx = vy = 0.0;
    x = random(-500,500);
    y = random(-500,500);
    this.mass = mass;
    this.radius = radius;
  }
  void interact() {
    if (!gotOne && !overOne) {
      float xdiff = mouseX - screenX(x,y,0);
      float ydiff = mouseY - screenY(x,y,0);
      mouseOver = abs(xdiff)/sc < radius && abs(ydiff)/sc < radius;
    }
    else {
      mouseOver = false;
    }
    if ((mouseOver || selected) && !overOne) {
      overOne = true;
      if(mousePressed) {
        gotOne = true;
        selected = true;
        x = (mouseX-tx)/sc;
        y = (mouseY-ty)/sc;
      }
      else {
        selected = false;
        gotOne = false;
      }
    }
    else {
      fill(0,0,0,20);
      if (!mousePressed) {
        selected = false;
        gotOne = false;
      }
    }
  }
  void draw() {
    interact();
    if (mouseOver || selected) {
      fill(255,0,0,20);
    }
    else {
      fill(0,0,0,20);
    }
    noStroke();
    ellipseMode(CENTER_RADIUS);
    ellipse(x,y,radius,radius);
  }
  void update() {
    vx += fx / mass;
    vy += fy / mass;
    x += vx;
    y += vy;
    fx = 0;
    fy = 0;
    vx *= const_friction;
    vy *= const_friction;
  }
}

class DeliciousTag extends Node {
  String name;
  int count;
  int linkcount;
  DeliciousTag(String name, int count) {
    super(count*const_mass, (10+count)); // 11 + count?
    this.name = name;
    this.count = count;
  }
  void draw() {
    super.draw();
    fill(0);
    textFont(font, radius);
    textMode(ALIGN_CENTER);
    text(name, x, y);
  }
}

class DeliciousPost extends Node {
  Vector tags;
  DeliciousPost() {
    super();
    tags = new Vector();
  }
  void draw() {
    super.draw();  
  }
}

class Spring {
  Node a, b;
  float length = 50.0;
  float dist_offset = 0.0;
  Spring(Node a, Node b) {
    this.a=a;
    this.b=b;
  }
  void draw() {
    colorMode(HSB,1.0);
    stroke(max(min(0.5 + dist_offset/length,1.0),0.0),1,1,0.2);
    line(a.x,a.y,b.x,b.y);
    colorMode(RGB,255.0);
  }
  void update() {
    float dist = sqrt(sq(a.x - b.x) + sq(a.y - b.y));
    dist_offset = dist - length;
    float angle = atan2(a.x - b.x, a.y - b.y);
    a.fx -= sin(angle) * dist_offset;
    a.fy -= cos(angle) * dist_offset;
    b.fx += sin(angle) * dist_offset;
    b.fy += cos(angle) * dist_offset;
  }
}

Vector nodes, links;
float minx, maxx, miny, maxy;

void setup() {
  hint(NEW_GRAPHICS);
  size(800,800);
  //smooth();
  links = new Vector();
  nodes = new Vector();
  hackyParser();
  font = loadFont("Verdana.vlw");
}

void loop() {
  background(255);
  // assess forces
  for (int i = 0; i < links.size(); i++){
    Spring s = (Spring)links.get(i);
    s.update();
  }
  // collide forces
  for (int i = 0; i < nodes.size(); i++) {
    Node a = (Node)nodes.get(i);
    for (int j = i+1; j < nodes.size(); j++) {
      Node b = (Node)nodes.get(j);
      float dist_offset = sqrt(sq(a.x - b.x) + sq(a.y - b.y)) - 3*(a.radius+b.radius);
      if (dist_offset < 0) {
        float angle = atan2(a.x - b.x, a.y - b.y);
        a.fx -= sin(angle) * dist_offset;
        a.fy -= cos(angle) * dist_offset;
        b.fx += sin(angle) * dist_offset;
        b.fy += cos(angle) * dist_offset;
      }
    }
  }
  // apply forces
  for (int i = 0; i < nodes.size(); i++) {
    Node node = (Node)nodes.get(i);
    node.update();
    minx = i == 0 ? node.x : min(minx, node.x);  
    miny = i == 0 ? node.y : min(miny, node.y);  
    maxx = i == 0 ? node.x : max(maxx, node.x);
    maxy = i == 0 ? node.y : max(maxy, node.y);
  }
  float w = (maxx-minx)+100;
  float h = (maxy-miny)+100;
  float x = minx-50;
  float y = miny-50;
  if (!mousePressed) {
    tx = (width/2)-(x+(w/2));
    ty = (height/2)-(y+(h/2));
    sc += (min(width/w,height/h) - sc) / 5.0;
  }
  translate( tx, ty, 0 );
  scale(sc);
  overOne = false;
  for (int i = 0; i < nodes.size(); i++) {
    Node node = (Node)nodes.get(i);
    node.draw();
  }
  for (int i = 0; i < links.size(); i++) {
    Spring c = (Spring)links.get(i);
    c.draw();
  }
  cursor(overOne ? MOVE : ARROW);
}

void hackyParser() {

  Map tagMap = new HashMap();

  Vector tags = new Vector();
  // parse tags...
  String lines[] = loadStrings("tags.xml");
  for (int i=2; i < lines.length-2; i++) {
    int namePos = lines[i].lastIndexOf("tag=\"")+5;
    int endNamePos = lines[i].lastIndexOf('\"');
    String name = lines[i].substring(namePos,endNamePos);
    int tagPos = lines[i].indexOf("count=\"")+7;
    int endTagPos = lines[i].lastIndexOf("\" tag");
    int count = Integer.parseInt(lines[i].substring(tagPos,endTagPos));;
    DeliciousTag tag = new DeliciousTag(name, count);
    tags.add(tag);
    tagMap.put(name, tag);
  }

  Vector posts = new Vector();
  // parse posts...
  lines = loadStrings("posts.xml");
  for (int i=2; i < lines.length-2; i++) {
    int tagPos = lines[i].indexOf("tag=\"")+5;
    int endTagPos = lines[i].indexOf("\" description");
    String tagString = lines[i].substring(tagPos,endTagPos);
    String list[] = split(tagString);
    DeliciousPost p = new DeliciousPost();
    for (int j = 0; j < list.length; j++) {
      p.tags.add(tagMap.get(list[j]));
    }
    posts.add(p);
  }
  
  // create connections
  for (int i = 0; i < posts.size(); i++) {
    DeliciousPost p = (DeliciousPost)posts.get(i);
    for (int j = 0; j < p.tags.size(); j++) {
      DeliciousTag t = (DeliciousTag)p.tags.get(j);
      links.add(new Spring(p,t));
    }
  }
  
  // populate nodes
  for (int i = 0; i < posts.size(); i++) {
    DeliciousPost p = (DeliciousPost)posts.get(i);
    nodes.add(p);
    float angle = TWO_PI*(float)i/(float)posts.size();
    p.x = 200.0*cos(angle);
    p.y = 200.0*sin(angle);
  }
  for (int i = 0; i < tags.size(); i++) {
    DeliciousTag t = (DeliciousTag)tags.get(i);
    nodes.add(t);
    float angle = TWO_PI*(float)i/(float)posts.size();
  }
  
}

